// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tracer

import (
	"context"
	"fmt"
	"gitee.com/go-wares/log/base"
	"gitee.com/go-wares/log/conf"
	"net/http"
	"strconv"
	"sync"
	"time"
)

var (
	// RootCtx
	// is a context with background.
	RootCtx = context.Background()

	spanPool = &sync.Pool{
		New: func() any {
			return &span{}
		},
	}

	spanTime = time.Unix(0, 0)
)

type (
	span struct {
		attr               base.Attr
		ctx                context.Context
		logs               []*base.Item
		manager            any
		name               string
		startTime, endTime time.Time
		tracing            *base.Tracing
	}

	spanLogger interface {
		LogItem(item *base.Item)
	}

	spanReporter interface {
		Report(span base.Span)
	}
)

// Child
// creates a child span.
func (o *span) Child(name string) base.Span {
	s := spanPool.Get().(*span).init(o.manager, name)
	s.tracing = o.tracing.Child()
	s.ctx = context.WithValue(o.ctx, base.TracingOnContext, s.tracing)
	return s
}

// Context
// returns the context of the span.
func (o *span) Context() context.Context {
	return o.ctx
}

// Duration
// returns the duration of the span.
func (o *span) Duration() time.Duration {
	return o.endTime.Sub(o.startTime)
}

// End
// ends the span.
func (o *span) End() {
	o.endTime = time.Now()

	// Copy
	// logs in span then send to logging system.
	if v, ok := o.manager.(spanLogger); ok {
		for _, item := range o.logs {
			v.LogItem(item.Copy())
		}
	}

	// Report
	// span to reporter.
	if v, ok := o.manager.(spanReporter); ok {
		v.Report(o)
	}
}

// EndTime
// returns the end time of the span.
func (o *span) EndTime() time.Time {
	return o.endTime
}

// Logs
// returns the logs of the span.
func (o *span) Logs() []*base.Item {
	return o.logs
}

// Name
// returns the name of the span.
func (o *span) Name() string {
	return o.name
}

// Release
// put the span into the pool.
func (o *span) Release() {
	o.uninit()
	spanPool.Put(o)
}

// StartTime
// returns the start time of the span.
func (o *span) StartTime() time.Time {
	return o.startTime
}

// Tracing
// returns the tracing component of the span.
func (o *span) Tracing() *base.Tracing {
	return o.tracing
}

// +---------------------------------------------------------------------------+
// | Attributes methods                                                        |
// +---------------------------------------------------------------------------+

// GetAttr
// returns the attribute of the span.
func (o *span) GetAttr() base.Attr {
	return o.attr
}

// SetAttr
// sets the attribute of the span.
func (o *span) SetAttr(key string, value any) {
	o.attr[key] = value
}

// +---------------------------------------------------------------------------+
// | Logger methods                                                            |
// +---------------------------------------------------------------------------+

// Debug
// send debug level log with standard format.
func (o *span) Debug(format string, args ...any) {
	if conf.Get().DebugOn() {
		o.logs = append(o.logs, base.NewItem(o.ctx, nil, base.LevelDebug, format, args...))
	}
}

// Info
// send info level log with standard format.
func (o *span) Info(format string, args ...any) {
	if conf.Get().InfoOn() {
		o.logs = append(o.logs, base.NewItem(o.ctx, nil, base.LevelInfo, format, args...))
	}
}

// Warn
// send warn level log with standard format.
func (o *span) Warn(format string, args ...any) {
	if conf.Get().WarnOn() {
		o.logs = append(o.logs, base.NewItem(o.ctx, nil, base.LevelWarn, format, args...))
	}
}

// Error
// send error level log with standard format.
func (o *span) Error(format string, args ...any) {
	if conf.Get().ErrorOn() {
		o.logs = append(o.logs, base.NewItem(o.ctx, nil, base.LevelError, format, args...))
	}
}

// Fatal
// send fatal level log with standard format.
func (o *span) Fatal(format string, args ...any) {
	if conf.Get().FatalOn() {
		o.logs = append(o.logs, base.NewItem(o.ctx, nil, base.LevelFatal, format, args...))
	}
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

func (o *span) init(manager any, format string, args ...any) *span {
	o.attr = make(base.Attr)
	o.logs = make([]*base.Item, 0)
	o.manager = manager
	o.name = fmt.Sprintf(format, args...)
	o.startTime, o.endTime = time.Now(), spanTime
	o.tracing = nil
	return o
}

func (o *span) uninit() {
	o.attr = nil
	o.ctx = nil
	o.logs = nil
	o.manager = nil
	o.tracing = nil
}

// +---------------------------------------------------------------------------+
// | Constructor methods                                                       |
// +---------------------------------------------------------------------------+

// NewSpan
// creates a span.
func NewSpan(manager any, format string, args ...any) base.Span {
	o := spanPool.Get().(*span).init(manager, format, args...)
	o.tracing = base.NewTracing()
	o.ctx = context.WithValue(RootCtx, base.TracingOnContext, o.tracing)
	return o
}

// NewSpanWithContext
// creates a span from context.
func NewSpanWithContext(ctx context.Context, manager any, format string, args ...any) base.Span {
	if ctx == nil {
		return NewSpan(manager, format, args...)
	}

	o := spanPool.Get().(*span).init(manager, format, args...)

	if v, ok := ctx.Value(base.TracingOnContext).(*base.Tracing); ok {
		o.tracing = v.Child()
	} else {
		o.tracing = base.NewTracing()
	}

	o.ctx = context.WithValue(ctx, base.TracingOnContext, o.tracing)
	return o
}

func NewSpanWithHeaders(manager any, header map[string]any, format string, args ...any) base.Span {
	o := spanPool.Get().(*span).init(manager, fmt.Sprintf(format, args...))

	if tid := header[conf.Get().OpentracingTraceId]; tid != nil {
		if sid := header[conf.Get().OpentracingSpanId]; sid != nil {
			var ver int32 = 0

			if s := header[conf.Get().OpentracingSpanPosition]; s != nil {
				if n, ne := strconv.ParseInt(fmt.Sprintf(`%v`, s), 10, 32); ne == nil {
					ver = int32(n)
				}
			}

			o.tracing = base.NewTracingWithHex(fmt.Sprintf(`%v`, tid), fmt.Sprintf(`%v`, sid), ver)
		}
	}

	if o.tracing == nil {
		o.tracing = base.NewTracing()
	}

	o.ctx = context.WithValue(RootCtx, base.TracingOnContext, o.tracing)
	return o
}

// NewSpanWithRequest
// creates a span from http request.
func NewSpanWithRequest(manager any, req *http.Request) base.Span {
	o := spanPool.Get().(*span).init(manager, req.URL.Path)

	if tid := req.Header.Get(conf.Get().OpentracingTraceId); tid != "" {
		if sid := req.Header.Get(conf.Get().OpentracingSpanId); sid != "" {
			var ver int32 = 0

			if s := req.Header.Get(conf.Get().OpentracingSpanPosition); s != "" {
				if n, ne := strconv.ParseInt(s, 10, 32); ne == nil {
					ver = int32(n)
				}
			}

			o.tracing = base.NewTracingWithHex(tid, sid, ver)
		}
	}

	if o.tracing == nil {
		o.tracing = base.NewTracing()
	}

	o.ctx = context.WithValue(req.Context(), base.TracingOnContext, o.tracing)
	o.SetAttr(base.AttrHeader, req.Header)
	o.SetAttr(base.AttrProtocol, req.Proto)
	o.SetAttr(base.AttrRequestMethod, req.Method)
	o.SetAttr(base.AttrRequestUrl, req.URL.Path)
	o.SetAttr(base.AttrUserAgent, req.UserAgent())
	return o
}
